package com.espirit.eap.util;

import java.io.File;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;

import javax.annotation.Resource;

import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.struts2.ServletActionContext;
import org.springframework.stereotype.Service;

import com.espirit.common.TxFile;
import com.espirit.eap.sa.LoginUser;
import com.espirit.eap.sa.LoginUserService;
import com.googlecode.cswish.Config;
import com.googlecode.cswish.model.ModelInfo;
import com.googlecode.cswish.util.ClassUtil;
import com.googlecode.cswish.util.FrameConstant;
import com.googlecode.cswish.util.ITxFileService;

/**
 * 2 kinds of transaction model:<br>
 * <p>
 * WAY 1: <br>
 * Store the file in the folder 'site/xxx/tmp/', move them to 'site/xxx/' after committed
 * (site <-- config.getSiteRootPath())
 * <p>
 * WAY 2: <br>
 * Use TxFile object and store the file in the folder 'site/xxx/upload/', the file with
 *  transaction id (use System.currentTimeMillis() as the transaction id)
 * <p>
 * 
 * 关于文件事务可选的流程有:
 * 1) convertToTxFile --> prepareTxFile --> resumeTxFiles --> commitTxFile
 * 2) convertToTxFile --> commitTxFile
 * 
 * @author Jerry
 *
 */
@Service
public class TxFileService implements ITxFileService {
	
	private static final Log logger = LogFactory.getLog(TxFileService.class);
	
	@Resource
	private Config config;
	
	@Resource
	private Functions functions;
	
	@Resource
	private LoginUserService loginUserService;
	
	private boolean useUserIdAsTxId = true;
	
	private String getTxId(String txId) {
		if (txId == null || txId.length() == 0) {
			if (useUserIdAsTxId) {
				LoginUser user =  loginUserService.getLoginUser();
				if (user == null) {
					txId = "0";
				} else {
					txId = String.valueOf(user.getId());
				}
			} else {
				logger.error("Invalid tx id: " + txId);
			}
		}
		return txId;
	}
	
	public String begin(String txId) {
		txId = getTxId(txId);
		
		// prepare the temporary directory
		String tempFilePath = config.getRealSiteTmpPath() + '/' + txId;
		File tempDirFile = new File(tempFilePath);
		if (tempDirFile.exists()) {
			try {
				for (File file : tempDirFile.listFiles()) {
					if (file.isDirectory()) {
						FileUtils.deleteDirectory(file);
					} else {
						file.delete();
					}
				}
			} catch (Exception ex) {
				logger.error("Failed to clean folder: " + tempFilePath, ex);
			}
		} else {
			tempDirFile.mkdirs();
		}
		
		return txId;
	}
	
	public boolean isSame(String txId, String filename) {
		if (filename == null || filename.length() == 0) {
			return true;
		}
		txId = getTxId(txId);
		
		String webRootPath = new File(config.getRealSitePath()).getPath();
		
		String tempFilePath = config.getRealSiteTmpPath(txId) + (filename.charAt(0) == '/' ? "" : '/') + filename;
		File tempFile = new File(tempFilePath);
		if (!tempFile.exists()) {
			// no temp file --> no change (same)
			return true;
		}
		
		String realFilePath = webRootPath + (filename.charAt(0) == '/' ? "" : '/') + filename;
		File realFile = new File(realFilePath);
		if (!realFile.exists()) {
			// no real file --> changed (different)
			return false;
		}
		
		try {
			String tempContent = FileUtils.readFileToString(tempFile, "UTF-8");
			String realContent = FileUtils.readFileToString(realFile, "UTF-8");
			return tempContent.equals(realContent);
		} catch (Exception ex) {
			logger.error("Failed to read file: " + filename, ex);
			return true;
		}
	}
	
	public String read(String txId, String filename) {
		if (filename == null || filename.length() == 0 || filename.startsWith(":")) {
			return null;
		}
		txId = getTxId(txId);
		
		String siteWebRootPath = new File(config.getRealSitePath()).getPath();
		
		// temporary directory --> real site directory --> real web root directory
		String tempFilePath = config.getRealSiteTmpPath(txId) + (filename.charAt(0) == '/' ? "" : '/') + filename;
		File file = new File(tempFilePath);
		if (!file.exists()) {
			tempFilePath = siteWebRootPath + (filename.charAt(0) == '/' ? "" : '/') + filename;
			file = new File(tempFilePath);
		}
		String realWebRootPath = config.getRealWebRootPath();
		if (!file.exists() && filename.startsWith("static")) {
			// try to load template file from the web root directory
			file = new File(realWebRootPath + (filename.charAt(0) == '/' ? "" : '/') + filename);
		}
		
		// get the file content
		String content = null;
		InputStream inputStream = null;
		try {
			String path = file.getPath();
			if (path.startsWith(siteWebRootPath) || path.startsWith(realWebRootPath + File.separatorChar + "static" + File.separatorChar)) {
				if (file.isFile() && file.exists()) {
					content = FileUtils.readFileToString(file, "UTF-8");
				} else {
					inputStream = this.getClass().getResourceAsStream(filename);
					if (inputStream != null) {
						content = IOUtils.toString(inputStream, "utf-8");
					} else {
						logger.debug("File doesn't exist: " + file.getPath());
					}
				}
			} else {
				logger.error("Illegal path: " + file.getPath());
			}
		} catch (Exception ex) {
			logger.error("Failed to read file: " + file.getPath(), ex);
		} finally {
			IOUtils.closeQuietly(inputStream);
		}
		return content;
	}
	
	public void write(String txId, String filename, byte[] content) {
		if (filename == null || filename.length() == 0) {
			return ;
		}
		txId = getTxId(txId);
		
		String webRootPath = new File(config.getRealSitePath()).getPath();
		String tempFilePath = config.getRealSiteTmpPath(txId) + (filename.charAt(0) == '/' ? "" : '/') + filename;
		
		File file = new File(tempFilePath);
		try {
			if (file.getPath().startsWith(webRootPath)) {
				if (!file.exists() && (content == null || content.length == 0)) {
					// for better performance, do nothing, don't create the empty file
				} else {
					FileUtils.writeByteArrayToFile(file, content);
				}
			} else {
				logger.error("Illegal path: " + file.getPath());
			}
		} catch (Exception ex) {
			logger.error("Failed to write file: " + file.getPath(), ex);
		}
	}
	
	// write content to temporary file
	public void write(String txId, String filename, String content) {
		try {
			write(txId, filename, content.getBytes("UTF-8"));
		} catch (Exception ex) {
			logger.error("Failed to write file: " + filename, ex);
		}
	}
	
	public void commit(String txId, boolean enableOverride) {
		txId = getTxId(txId);
		
		String realSitePath = config.getRealSitePath();
		String tempPath = config.getRealSiteTmpPath(txId);
		File tempPathDir = new File(tempPath);
		tempPath = tempPathDir.getPath();
		if (tempPathDir.exists()) {
			File realPathDir = new File(realSitePath);
			File rootDir = new File(config.getRealWebRootPath());
			String taglibDir = "/pages/taglib/".replace('/', File.separatorChar);
			for (File file : FileUtils.listFiles(tempPathDir, null, true)) {
				if (!file.isDirectory()) {
					// move all the temporary files to real folder
					String filename = file.getPath().substring(tempPath.length());
					File dest;
					// special logic for the taglib folder 	TODO: better solution?
					if (filename.startsWith(taglibDir)) {
						dest = new File(rootDir, filename);
					} else {
						dest = new File(realPathDir, filename);
					}
					try {
						if (enableOverride) {
							if (dest.exists()) {
								boolean ret = dest.delete();
								if (ret) {
									FileUtils.moveFile(file, dest);
								} else {
									FileUtils.writeByteArrayToFile(dest, FileUtils.readFileToByteArray(file));
								}
							} else {
								FileUtils.moveFile(file, dest);
							}
						} else {
							if (dest.exists()) {
								logger.info("Skip upgrade the existed file: " + dest.getPath());
							} else {
								FileUtils.moveFile(file, dest);
							}
						}
					} catch (Exception ex) {
						logger.error("Failed to commit file to : " + dest.getName(), ex);
					}
				}
			}
		}
	}
	
	public String getTempDir(String url) {
		String txId = getTxId(null);
		String newUrl = config.getRealSiteTmpPath(txId) + (url.charAt(0) == '/' ? "" : '/') + url;
		return newUrl;
	}

	// @see getRealTempDir
	public String getRealTxFilePath(String url, boolean shareByAllSite) {
		String txId = getTxId(null);
		
		if (url.charAt(0) != '/') {
			url = '/' + url;
		}
		String tempDir = config.getRealSiteTmpPath(txId);
		String tempFilePath = tempDir + url;
		File file = new File(tempFilePath);
		String newUrl;
		if (!file.exists()) {
			newUrl = shareByAllSite ? url : config.getSitePath() + url;
		} else {
			newUrl = tempDir + url;
		}
		return newUrl;
	}
	
	public String checkAndGetRealFilePath(String url) {
		if (url.charAt(0) != '/') {
			url = '/' + url;
		}
		
		File file = new File(config.getRealWebRootPath() + url);
		String realPath;
		if (!file.exists()) {
			file = new File(config.getRealSitePath() + url);
			if (!file.exists()) {
				realPath = null;
			} else {
				realPath = file.getPath();
			}
		} else {
			realPath = file.getPath();
		}
		return realPath;
	}
	
	
	public TxFile convertToTxFile(Object model, String fullFilePropertyName, String filePropertyName, File file) {
		// TODO: remove old file, see UploadFileService.setFile(Class clazz, RelatedFile model, String identify)
		String destSubDir;
		if (model instanceof Map) {
			destSubDir = "";
		} else {
			destSubDir = ModelInfo.getStaticClassName(ClassUtil.getClass(model).getSimpleName()) + "/";
		}
		Object theName = functions.val(model, filePropertyName);
		if (theName == null || theName instanceof String) {
			String filename = (String) theName;
		
			boolean processed = filename != null && filename.startsWith(config.getUploadDir());
			if (!processed) {
				String destPicFileName = prepareToDestFile(file, destSubDir, filename);
				functions.setVal(model, filePropertyName, destPicFileName);
				File destFile = new File(config.getRealSitePath() + destPicFileName);
				
				TxFile txFile = new TxFile(file, destFile, fullFilePropertyName, destPicFileName);
				// Cache the tx file to current session, use the target file name as the key
				getTxFileCache().put(destFile.getName(), txFile);
				return txFile;
			}
		}
		return null;
	}
	
	
	
	private Map<String, TxFile> getTxFileCache() {
		Map<String, Object> session = ServletActionContext.getContext().getSession();
		if (session != null) {
			Map<String, TxFile> cache = (Map<String, TxFile>) session.get(FrameConstant.SESSION_TX_FILECACHE);
			if (cache == null) {
				cache = new HashMap<String, TxFile>();
				session.put(FrameConstant.SESSION_TX_FILECACHE, cache);
			}
			return cache;
		} else {
			return new HashMap<String, TxFile>(2);
		}
	}
	
	private String prepareToDestFile(File file, String destSubDir, String inputFileName) {
		// put the dest file to '/upload/'
		String destFilePrefix = config.getUploadDir();
		
		// use UUID as the file name
		String uuid = StringUtils.remove(UUID.randomUUID().toString(), '-');
		String suffix = null;
		if (inputFileName != null) {
			suffix = inputFileName.substring(inputFileName.lastIndexOf('.') + 1);
			// check and format the suffix		TODO: necessary?
			StringBuilder sb = new StringBuilder();
			for (int i = 0; i < suffix.length(); i++) {
				char c = suffix.charAt(i);
				if (c == '?' || c == '/' || c == '\\') {
					break;
				} else {
					sb.append(c);
				}
			}
			suffix = sb.toString();
		}
		
		suffix = inputFileName;
		
		String newName;
		if (suffix == null || suffix.length() == 0) {
			newName = destFilePrefix + destSubDir + uuid;
		} else {
			newName = destFilePrefix + destSubDir + uuid + '.' + suffix;
		}
		return newName;
	}
	
	/**
	 * Move the txFile to temporary folder for the second commit
	 * 
	 * @param txFiles
	 */
	public void prepareTxFile(List<TxFile> txFiles) {
		String txId = getTxId(null);
		// prepare the temporary directory
		String tempFilePath = config.getRealSiteTmpPath() + '/' + txId + "_tx";
		File tempDirFile = new File(tempFilePath);
		if (!tempDirFile.exists()) {
			tempDirFile.mkdirs();
		}
		
		for (TxFile txFile : txFiles) {
			txFile.setEnableAutoCommit(false);
			
			File from = txFile.getFrom();
			File prepareFrom = new File(tempDirFile, txFile.getTo().getName());
			from.renameTo(prepareFrom);
			txFile.setFrom(prepareFrom);
		}
	}

	public void commitTxFile(List<TxFile> txFiles) {
		Map<String, TxFile> fileCache = getTxFileCache();
		
		for (TxFile txFile : txFiles) {
			if (txFile.isEnableAutoCommit()) {
				// commit the file and remove it from cache 
				fileCache.remove(txFile.getTo().getName());
				txFile.comit();
			}
		}
	}

	public List<TxFile> resumeTxFiles(Object model, List<TxFile> txFiles, String txFileName) {
		if (logger.isDebugEnabled()) {
			logger.debug("resumeTxFiles, txFileName=" + txFileName);
		}
		String[] txFileNames = StringUtils.split(txFileName, ':');
		if (txFiles == null) {
			txFiles = new ArrayList<TxFile>(txFileNames.length);
		}
		
		Map<String, TxFile> fileCache = getTxFileCache();
		for (String filename : txFileNames) {
			TxFile txFile = fileCache.get(filename);
			if (txFile != null) {
				if (logger.isDebugEnabled()) {
					logger.debug("write tx file property: " + txFile.getFullFilePropertyName() + "=" + txFile.getDestPicFileName());
				}
				functions.setVal(model, txFile.getFullFilePropertyName(), txFile.getDestPicFileName());
				txFile.setEnableAutoCommit(true);
				txFiles.add(txFile);
			} else {
				logger.error("Failed to find tx file:" + filename);
			}
		}
		return txFiles;
	}
}